package bsh.meta;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.objectweb.asm.Opcodes;

import bsh.ReflectionUtils;
import bsh.TypeResolver;
import bsh.asm.acessor.ExternalVariableObjectAccessor;
import bsh.asm.acessor.FieldObjectAcessor;
import bsh.asm.acessor.LocalObjectAccessor;
import bsh.asm.acessor.ObjectAccessor;
import bsh.asm.acessor.StaticFieldObjectAccessor;
import bsh.asm.acessor.ThisFieldObjectAcessor;
import bsh.impl.CachedTypeResolver;

/**
 * A scope that represents an inner class scope. Has a reference to the scope where the inner class was declared in order to access local
 * variables and other variables visible in the enclosing scope
 * 
 * @author takeshi
 */
public class InnerTypeGlobalScope extends ScopeImpl {

    private final Scope declaringScope;

    private final UnfinishedInnerType unfinishedType;

    private int variableCounter;

    private final Map<String, String> externalVariableMapping = new LinkedHashMap<String, String>();

    private final TypeResolver typeResolver;

    /**
     * Constructor
     * 
     * @param declaringScope the declaring scope. Is not this class's parent scope
     * @param unfinishedType {@link UnfinishedInnerType}
     */
    public InnerTypeGlobalScope(Scope declaringScope, UnfinishedInnerType unfinishedType) {
        super();
        this.declaringScope = declaringScope;
        this.unfinishedType = unfinishedType;
        this.typeResolver = new CachedTypeResolver(declaringScope);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ObjectAccessor accessorNotFound(String name) {
        ObjectAccessor accessible = declaringScope.getAccessor(name);
        if (accessible != null) {
            ObjectAccessor adapted = adaptAccessor(accessible, name);
            this.addAccessor(name, adapted);
            return getAccessor(name);
        }
        return null;
    }

    /**
     * Adapts an object accessor from the enclosing scope
     * 
     * @param accessible {@link ObjectAccessor}
     * @param name the name of the variable
     * @return {@link ObjectAccessor}
     */
    private ObjectAccessor adaptAccessor(ObjectAccessor accessible, String name) {
        if (accessible instanceof LocalObjectAccessor) {
            String variableName = selectNewFieldName(name);
            FieldDescriptor field =
                    this.unfinishedType.addField(variableName, accessible.type(), Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC
                            | Opcodes.ACC_FINAL);
            return new ThisFieldObjectAcessor(unfinishedType, field);
        } else if (accessible instanceof ThisFieldObjectAcessor) {
            ThisFieldObjectAcessor outer = (ThisFieldObjectAcessor) accessible;
            FieldDescriptor outerField = ((FieldObjectAcessor) outer.getFieldAccessor()).getField();
            FieldDescriptor field = this.unfinishedType.getEnclosingInstanceField();
            return new ExternalVariableObjectAccessor(new ThisFieldObjectAcessor(unfinishedType, field), outerField.fieldName(),
                    accessible.type());
        } else if (accessible instanceof StaticFieldObjectAccessor) {
            return accessible; // nothing necessary
        } else if (accessible instanceof ExternalVariableObjectAccessor) {
            FieldDescriptor field = this.unfinishedType.getEnclosingInstanceField();
            ThisFieldObjectAcessor enclosingInstanceField = new ThisFieldObjectAcessor(this.unfinishedType, field);
            ObjectAccessor accessor = prepend(enclosingInstanceField, (ExternalVariableObjectAccessor) accessible, field);
            return accessor;
        }
        throw new IllegalStateException();
    }

    /**
     * Creates a new synthetic field name
     * 
     * @param originalVariable the original variable name
     * @return String
     */
    private String selectNewFieldName(String originalVariable) {
        String variableName = "_val$" + variableCounter++;
        if (this.externalVariableMapping.containsKey(variableName)) {
            return selectNewFieldName(originalVariable);
        }
        this.externalVariableMapping.put(originalVariable, variableName);
        return variableName;
    }

    /**
     * Prepends an {@link ExternalVariableObjectAccessor} to an {@link ObjectAccessor}, exposing it as an instance field.
     * 
     * @param thisFieldObjectAcessor {@link ThisFieldObjectAcessor}
     * @param accessible the {@link Object} to be prepended
     * @param field the {@link FieldDescriptor}
     * @return {@link ObjectAccessor}
     */
    private ObjectAccessor prepend(ThisFieldObjectAcessor thisFieldObjectAcessor, ObjectAccessor accessible, FieldDescriptor field) {
        if (accessible instanceof ThisFieldObjectAcessor) {
            FieldDescriptor linkField = ((FieldObjectAcessor) ((ThisFieldObjectAcessor) accessible).getFieldAccessor()).getField();
            return new ExternalVariableObjectAccessor(thisFieldObjectAcessor, linkField.fieldName(), linkField.fieldType());
        } else if (accessible instanceof ExternalVariableObjectAccessor) {
            ExternalVariableObjectAccessor externalAccessor = (ExternalVariableObjectAccessor) accessible;
            return new ExternalVariableObjectAccessor(prepend(thisFieldObjectAcessor, externalAccessor.getContainer(), field),
                    externalAccessor.getFieldName(), accessible.type());
        }
        throw new IllegalStateException();
    }

    /**
     * @return the externalVariableMapping
     */
    public Map<String, String> getExternalVariableMapping() {
        return externalVariableMapping;
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper resolveType(String name) {
        return typeResolver.resolveType(name);
    }

    /**
     * {@inheritDoc}
     */
    public void addTypeAlias(String name, TypeWrapper type) {
        typeResolver.addTypeAlias(name, type);
    }

    /**
     * {@inheritDoc}
     */
    public void addWildcardImport(String packageName) {
        typeResolver.addWildcardImport(packageName);
    }

    /**
     * {@inheritDoc}
     */
    public ReflectionUtils getReflectionUtils() {
        return typeResolver.getReflectionUtils();
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, TypeWrapper> getImports() {
        return typeResolver.getImports();
    }

    /**
     * {@inheritDoc}
     */
    public Set<String> getWildCards() {
        return typeResolver.getWildCards();
    }

}
